import os
import sys
import subprocess

import chardet
from typing import List, AnyStr, Callable, Union, IO


class Device(object):
    _SCRIPT_DIR: str = os.path.dirname(sys.argv[0])

    @staticmethod
    def script_dir() -> str:
        return Device._SCRIPT_DIR

    @staticmethod
    def exec_raw(cmd: str) -> List[str]:
        proc = os.popen(cmd)
        lines = proc.buffer.readlines()
        return [Device.decode(line) for line in lines]

    @staticmethod
    def decode(string: AnyStr):
        if isinstance(string, bytes) or isinstance(string, bytearray):
            encoding = chardet.detect(string)['encoding']
            return string.decode(encoding if encoding else 'utf8')
        else:
            return string

    @staticmethod
    def exec_raw_async(cmd: str, std_in: Union[None, str], on_line: Union[None, Callable[[Union[None, str]], None]]):
        proc = subprocess.Popen(cmd, shell=True,
                                stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
        if std_in:
            proc.stdin.write(std_in.encode())
            proc.stdin.flush()
        line = proc.stdout.readline()
        if on_line is None:
            return

        line = Device.decode(line)
        while line != '':
            on_line(line)
            line = proc.stdout.readline()
            line = Device.decode(line)

        on_line(None)  # eof

    @staticmethod
    def shell(cmd: str) -> AnyStr:
        proc = subprocess.Popen(cmd,
                                stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
                                creationflags=subprocess.CREATE_NO_WINDOW)
        out, _ = proc.communicate()
        return out

    @staticmethod
    def async_shell(cmd: str) -> (IO[AnyStr], IO[AnyStr], subprocess.Popen):
        proc = subprocess.Popen(cmd,
                                stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT,
                                creationflags=subprocess.CREATE_NO_WINDOW)
        return Device.ShellIn(proc.stdin), Device.ShellOut(proc.stdout), proc

    class ShellIn(object):
        def __init__(self, io: IO[AnyStr]):
            self.io = io

        @staticmethod
        def encode(str_in: str) -> bytes:
            if isinstance(str_in, str):
                return str_in.encode()
            elif isinstance(str_in, bytes):
                return str_in
            else:
                return str(str_in).encode()

        def write(self, str_in: str):
            self.io.write(Device.ShellIn.encode(str_in))

        def writeline(self, line: str):
            self.io.write(Device.ShellIn.encode(line))
            self.io.write(b'\n')

        def writelines(self, lines: List[str]):
            self.io.writelines([Device.ShellIn.encode(line) for line in lines])

        def flush(self):
            self.io.flush()

    class ShellOut(object):
        def __init__(self, io: IO[AnyStr]):
            self.io = io

        @staticmethod
        def decode(string: AnyStr):
            if isinstance(string, bytes) or isinstance(string, bytearray):
                encoding = chardet.detect(string)['encoding']
                return string.decode(encoding if encoding else 'utf8')
            else:
                return string

        def read(self, n: int = -1) -> str:
            return Device.ShellOut.decode(self.io.read(n))

        def readline(self) -> str:
            return Device.ShellOut.decode(self.io.readline())

        def readlines(self) -> List[str]:
            return [Device.ShellOut.decode(line) for line in self.io.readlines()]
